﻿using System.Reflection.Emit;
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Linq;
using ProxyViewModel.Attributes;
using System.Collections.ObjectModel;

namespace ProxyViewModel.Emitors {
	public class PropertyValidationEmitor : EmitorBase {
		public PropertyValidationEmitor(TypeBuilder typeBuilder, Type operatable, IDictionary<string, object> parameters)
			: base(typeBuilder, operatable, parameters) { }

		protected override void EmitContent() {
			bool hasValidators = false;
			foreach (PropertyInfo info in ReflectionHelper.GetPublicVirtualProperties(Operatable)) {
				ValidatorBaseAttribute[] validators = info.GetCustomAttributes(true).OfType<ValidatorBaseAttribute>().ToArray();
				if (validators.Length > 0) {
					validators.ToList().ForEach(e => {
						ValidationManager.StoreValidator(TypeBuilder, info.Name, e);
					});
					GenerateValidationProperties(info);
					hasValidators = true;
				}
			}
			if (hasValidators) {
				GenerateValidationMethod();
			}
		}

		private void GenerateValidationMethod() {
			MethodBuilder builder = TypeBuilder.DefineMethod("IsValid", MethodAttributes.Private | MethodAttributes.Final,
					typeof(bool), new Type[] { typeof(string), typeof(object) });
			ILGenerator generator = builder.GetILGenerator();
			generator.DeclareLocal(typeof(bool));
			generator.Emit(OpCodes.Ldarg_0);
			generator.Emit(OpCodes.Call, Operatable.GetMethod("GetType"));
			generator.Emit(OpCodes.Ldarg_1);
			generator.Emit(OpCodes.Ldarg_2);
			generator.Emit(OpCodes.Ldarg_0);
			generator.Emit(OpCodes.Call, typeof(ValidationManager).GetMethod("IsValid"));
			generator.Emit(OpCodes.Stloc_0);
			Label lbl = generator.DefineLabel();
			generator.Emit(OpCodes.Br_S, lbl);
			generator.MarkLabel(lbl);
			generator.Emit(OpCodes.Ldloc_0);
			generator.Emit(OpCodes.Ret);

			CurrentParameters[Parameters.ValidationFunctionKey] = builder;
		}

		private void GenerateValidationProperties(PropertyInfo info) {
			string lastValidationErrorPropertyName = string.Format("{0}LastValidationMessage", info.Name);
			string isValidPropertyName = string.Format("IsValid{0}", info.Name);
			string validationErrorsProperty = string.Format("{0}ValidationErrors", info.Name);

			PropertyBuilder lastValidationErrorProperty = TypeBuilder.DefineProperty(lastValidationErrorPropertyName, PropertyAttributes.HasDefault, typeof(string), new Type[] { typeof(string) });
			PropertyBuilder isValidProperty = TypeBuilder.DefineProperty(isValidPropertyName, PropertyAttributes.HasDefault, typeof(bool), new Type[] { typeof(bool) });
			PropertyBuilder validationErrors = TypeBuilder.DefineProperty(validationErrorsProperty, PropertyAttributes.HasDefault, typeof(ObservableCollection<string>), new Type[] { typeof(ObservableCollection<string>) });

			GenerateSetterAndGetterForValidationMessageProperty(lastValidationErrorProperty);
			GenerateSetterAndGetterForIsValidProperty(isValidProperty);
			GenerateValidationMessagesProperty(validationErrors);
		}

		private void GenerateSetterAndGetterForValidationMessageProperty(PropertyBuilder lastValidationErrorProperty) {
			FieldBuilder field = TypeBuilder.DefineField(string.Format("_{0}", lastValidationErrorProperty.Name), typeof(string), FieldAttributes.Private);

			MethodBuilder getMethod = TypeBuilder.DefineMethod(string.Format("get_{0}", lastValidationErrorProperty.Name), MethodAttributes.Public |
				MethodAttributes.SpecialName | MethodAttributes.HideBySig, typeof(string), Type.EmptyTypes);

			ILGenerator il = getMethod.GetILGenerator();
			il.DeclareLocal(typeof(string));
			il.Emit(OpCodes.Nop);
			il.Emit(OpCodes.Ldarg_0);
			il.Emit(OpCodes.Ldfld, field);
			il.Emit(OpCodes.Stloc_0);
			Label lbl = il.DefineLabel();
			il.Emit(OpCodes.Br_S, lbl);
			il.MarkLabel(lbl);
			il.Emit(OpCodes.Ldloc_0);
			il.Emit(OpCodes.Ret);
			lastValidationErrorProperty.SetGetMethod(getMethod);
			lastValidationErrorProperty.SetSetMethod(GenerateObservableSetter(lastValidationErrorProperty, field));
		}

		private void GenerateSetterAndGetterForIsValidProperty(PropertyBuilder isValidProperty) {
			FieldBuilder field = TypeBuilder.DefineField(string.Format("_{0}", isValidProperty.Name), typeof(bool), FieldAttributes.Private);
			CallMethodOnConstruction(DefaultFiledToTrue(field));

			MethodBuilder getMethod = TypeBuilder.DefineMethod(string.Format("get_{0}", isValidProperty.Name), MethodAttributes.Public |
				MethodAttributes.SpecialName | MethodAttributes.HideBySig, typeof(bool), Type.EmptyTypes);

			ILGenerator il = getMethod.GetILGenerator();
			il.DeclareLocal(typeof(bool));
			il.Emit(OpCodes.Nop);
			il.Emit(OpCodes.Ldarg_0);
			il.Emit(OpCodes.Ldfld, field);
			il.Emit(OpCodes.Stloc_0);
			Label lbl = il.DefineLabel();
			il.Emit(OpCodes.Br_S, lbl);
			il.MarkLabel(lbl);
			il.Emit(OpCodes.Ldloc_0);
			il.Emit(OpCodes.Ret);
			isValidProperty.SetGetMethod(getMethod);
			isValidProperty.SetSetMethod(GenerateObservableSetter(isValidProperty, field));
		}

		private void GenerateValidationMessagesProperty(PropertyBuilder validationErrorsProperty) {
			FieldBuilder field = TypeBuilder.DefineField(string.Format("_{0}", validationErrorsProperty.Name), typeof(ObservableCollection<string>), FieldAttributes.Private);
			CallMethodOnConstruction(DeclareErrorMessagesCollection(field));
			MethodBuilder getMethod = TypeBuilder.DefineMethod(string.Format("get_{0}", validationErrorsProperty.Name),
				MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, typeof(ObservableCollection<string>), Type.EmptyTypes);

			ILGenerator il = getMethod.GetILGenerator();
			il.Emit(OpCodes.Nop);
			il.Emit(OpCodes.Ldarg_0);
			il.Emit(OpCodes.Ldfld, field);
			il.Emit(OpCodes.Nop);
			il.Emit(OpCodes.Ret);
			validationErrorsProperty.SetGetMethod(getMethod);
		}

		private MethodBuilder GenerateObservableSetter(PropertyBuilder info, FieldBuilder field) {
			MethodBuilder setMethod = TypeBuilder.DefineMethod(string.Format("set_{0}", info.Name), MethodAttributes.Public |
					MethodAttributes.SpecialName | MethodAttributes.HideBySig, null, new Type[] { info.PropertyType });
			ILGenerator il = setMethod.GetILGenerator();
			il.DeclareLocal(typeof(bool));
			il.Emit(OpCodes.Nop);
			il.Emit(OpCodes.Ldarg_1);
			il.Emit(OpCodes.Ldarg_0);
			il.Emit(OpCodes.Ldfld, field);
			il.Emit(OpCodes.Ceq);
			il.Emit(OpCodes.Stloc_0);
			il.Emit(OpCodes.Ldloc_0);
			Label lbl = il.DefineLabel();
			il.Emit(OpCodes.Brtrue_S, lbl);
			il.Emit(OpCodes.Nop);
			il.Emit(OpCodes.Ldarg_0);
			il.Emit(OpCodes.Ldarg_1);
			il.Emit(OpCodes.Stfld, field);
			PropertyOverrideEmitor.InvokeRaisePropertyChanged(il, info.Name, GetParameter<MethodInfo>(Parameters.RaisePropertyChangedMethodKey));
			il.MarkLabel(lbl);
			il.Emit(OpCodes.Ret);
			return setMethod;
		}

		public static MethodBuilder GenerateObservableSetter(PropertyBuilder info, FieldBuilder field, TypeBuilder typeBuilder, MethodInfo raisePropertyChanged,
			MethodAttributes attributes = MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig) {

			MethodBuilder setMethod = typeBuilder.DefineMethod(string.Format("set_{0}", info.Name), attributes, null, new Type[] { info.PropertyType });
			ILGenerator il = setMethod.GetILGenerator();
			il.DeclareLocal(typeof(bool));
			il.Emit(OpCodes.Nop);
			il.Emit(OpCodes.Ldarg_1);
			il.Emit(OpCodes.Ldarg_0);
			il.Emit(OpCodes.Ldfld, field);
			il.Emit(OpCodes.Ceq);
			il.Emit(OpCodes.Stloc_0);
			il.Emit(OpCodes.Ldloc_0);
			Label lbl = il.DefineLabel();
			il.Emit(OpCodes.Brtrue_S, lbl);
			il.Emit(OpCodes.Nop);
			il.Emit(OpCodes.Ldarg_0);
			il.Emit(OpCodes.Ldarg_1);
			il.Emit(OpCodes.Stfld, field);
			PropertyOverrideEmitor.InvokeRaisePropertyChanged(il, info.Name, raisePropertyChanged);
			il.MarkLabel(lbl);
			il.Emit(OpCodes.Ret);

			return setMethod;
		}

		private MethodInfo DeclareErrorMessagesCollection(FieldBuilder field) {
			MethodBuilder builder = TypeBuilder.DefineMethod(string.Format("Intialize{0}", field.Name), MethodAttributes.Private, typeof(void), Type.EmptyTypes);

			ILGenerator il = builder.GetILGenerator();
			il.Emit(OpCodes.Ldarg_0);
			il.Emit(OpCodes.Newobj, typeof(ObservableCollection<string>).GetConstructor(Type.EmptyTypes));
			il.Emit(OpCodes.Stfld, field);
			il.Emit(OpCodes.Ret);

			return builder;
		}

		private MethodInfo DefaultFiledToTrue(FieldBuilder field) {
			MethodBuilder builder = TypeBuilder.DefineMethod(string.Format("SetIsValid{0}", field.Name), MethodAttributes.Private, typeof(void), Type.EmptyTypes);

			ILGenerator il = builder.GetILGenerator();
			il.Emit(OpCodes.Ldarg_0);
			il.Emit(OpCodes.Ldc_I4_1);
			il.Emit(OpCodes.Stfld, field);
			il.Emit(OpCodes.Ret);

			return builder;
		}
	}
}
